"""
Jabber (XMPP) notification service.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.xmpp/
"""
from concurrent.futures import TimeoutError as FutTimeoutError
import logging
import mimetypes
import pathlib
import random
import string

import requests
import voluptuous as vol

from homeassistant.components.notify import (
    ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import (
    CONF_PASSWORD, CONF_RECIPIENT, CONF_RESOURCE, CONF_ROOM, CONF_SENDER)
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.template as template_helper

REQUIREMENTS = ['slixmpp==1.4.1']

_LOGGER = logging.getLogger(__name__)

ATTR_DATA = 'data'
ATTR_PATH = 'path'
ATTR_PATH_TEMPLATE = 'path_template'
ATTR_TIMEOUT = 'timeout'
ATTR_URL = 'url'
ATTR_URL_TEMPLATE = 'url_template'
ATTR_VERIFY = 'verify'

CONF_TLS = 'tls'
CONF_VERIFY = 'verify'

DEFAULT_CONTENT_TYPE = 'application/octet-stream'
DEFAULT_RESOURCE = 'home-assistant'
XEP_0363_TIMEOUT = 10

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_SENDER): cv.string,
    vol.Required(CONF_PASSWORD): cv.string,
    vol.Required(CONF_RECIPIENT): cv.string,
    vol.Optional(CONF_RESOURCE, default=DEFAULT_RESOURCE): cv.string,
    vol.Optional(CONF_ROOM, default=''): cv.string,
    vol.Optional(CONF_TLS, default=True): cv.boolean,
    vol.Optional(CONF_VERIFY, default=True): cv.boolean,
})


async def async_get_service(hass, config, discovery_info=None):
    """Get the Jabber (XMPP) notification service."""
    return XmppNotificationService(
        config.get(CONF_SENDER), config.get(CONF_RESOURCE),
        config.get(CONF_PASSWORD), config.get(CONF_RECIPIENT),
        config.get(CONF_TLS), config.get(CONF_VERIFY),
        config.get(CONF_ROOM), hass)


class XmppNotificationService(BaseNotificationService):
    """Implement the notification service for Jabber (XMPP)."""

    def __init__(self, sender, resource, password,
                 recipient, tls, verify, room, hass):
        """Initialize the service."""
        self._hass = hass
        self._sender = sender
        self._resource = resource
        self._password = password
        self._recipient = recipient
        self._tls = tls
        self._verify = verify
        self._room = room

    async def async_send_message(self, message="", **kwargs):
        """Send a message to a user."""
        title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
        text = '{}: {}'.format(title, message) if title else message
        data = kwargs.get(ATTR_DATA)
        timeout = data.get(ATTR_TIMEOUT, XEP_0363_TIMEOUT) if data else None

        await async_send_message(
            '{}/{}'.format(self._sender, self._resource),
            self._password, self._recipient, self._tls,
            self._verify, self._room, self._hass, text,
            timeout, data)


async def async_send_message(
        sender, password, recipient, use_tls, verify_certificate, room, hass,
        message, timeout=None, data=None):
    """Send a message over XMPP."""
    import slixmpp
    from slixmpp.exceptions import IqError, IqTimeout, XMPPError
    from slixmpp.xmlstream.xmlstream import NotConnectedError
    from slixmpp.plugins.xep_0363.http_upload import FileTooBig, \
        FileUploadError, UploadServiceNotFound

    class SendNotificationBot(slixmpp.ClientXMPP):
        """Service for sending Jabber (XMPP) messages."""

        def __init__(self):
            """Initialize the Jabber Bot."""
            super().__init__(sender, password)

            self.loop = hass.loop

            self.force_starttls = use_tls
            self.use_ipv6 = False
            self.add_event_handler(
                'failed_auth', self.disconnect_on_login_fail)
            self.add_event_handler('session_start', self.start)

            if room:
                self.register_plugin('xep_0045')  # MUC
            if not verify_certificate:
                self.add_event_handler('ssl_invalid_cert',
                                       self.discard_ssl_invalid_cert)
            if data:
                # Init XEPs for image sending
                self.register_plugin('xep_0030')  # OOB dep
                self.register_plugin('xep_0066')  # Out of Band Data
                self.register_plugin('xep_0071')  # XHTML IM
                self.register_plugin('xep_0128')  # Service Discovery
                self.register_plugin('xep_0363')  # HTTP upload

            self.connect(force_starttls=self.force_starttls, use_ssl=False)

        async def start(self, event):
            """Start the communication and sends the message."""
            # Sending image and message independently from each other
            if data:
                await self.send_file(timeout=timeout)
            if message:
                self.send_text_message()

            self.disconnect(wait=True)

        async def send_file(self, timeout=None):
            """Send file via XMPP.

            Send XMPP file message using OOB (XEP_0066) and
            HTTP Upload (XEP_0363)
            """
            if room:
                self.plugin['xep_0045'].join_muc(room, sender, wait=True)

            try:
                # Uploading with XEP_0363
                _LOGGER.debug("Timeout set to %ss", timeout)
                url = await self.upload_file(timeout=timeout)

                _LOGGER.info("Upload success")
                if room:
                    _LOGGER.info("Sending file to %s", room)
                    message = self.Message(sto=room, stype='groupchat')
                else:
                    _LOGGER.info("Sending file to %s", recipient)
                    message = self.Message(sto=recipient, stype='chat')

                message['body'] = url
                # pylint: disable=invalid-sequence-index
                message['oob']['url'] = url
                try:
                    message.send()
                except (IqError, IqTimeout, XMPPError) as ex:
                    _LOGGER.error("Could not send image message %s", ex)
            except (IqError, IqTimeout, XMPPError) as ex:
                _LOGGER.error("Upload error, could not send message %s", ex)
            except NotConnectedError as ex:
                _LOGGER.error("Connection error %s", ex)
            except FileTooBig as ex:
                _LOGGER.error(
                    "File too big for server, could not upload file %s", ex)
            except UploadServiceNotFound as ex:
                _LOGGER.error("UploadServiceNotFound: "
                              " could not upload file %s", ex)
            except FileUploadError as ex:
                _LOGGER.error("FileUploadError, could not upload file %s", ex)
            except requests.exceptions.SSLError as ex:
                _LOGGER.error("Cannot establish SSL connection %s", ex)
            except requests.exceptions.ConnectionError as ex:
                _LOGGER.error("Cannot connect to server %s", ex)
            except (FileNotFoundError,
                    PermissionError,
                    IsADirectoryError,
                    TimeoutError) as ex:
                _LOGGER.error("Error reading file %s", ex)
            except FutTimeoutError as ex:
                _LOGGER.error("The server did not respond in time, %s", ex)

        async def upload_file(self, timeout=None):
            """Upload file to Jabber server and return new URL.

            upload a file with Jabber XEP_0363 from a remote URL or a local
            file path and return a URL of that file.
            """
            if data.get(ATTR_URL_TEMPLATE):
                _LOGGER.debug(
                    "Got url template: %s", data[ATTR_URL_TEMPLATE])
                templ = template_helper.Template(
                    data[ATTR_URL_TEMPLATE], hass)
                get_url = template_helper.render_complex(templ, None)
                url = await self.upload_file_from_url(
                    get_url, timeout=timeout)
            elif data.get(ATTR_URL):
                url = await self.upload_file_from_url(
                    data[ATTR_URL], timeout=timeout)
            elif data.get(ATTR_PATH_TEMPLATE):
                _LOGGER.debug(
                    "Got path template: %s", data[ATTR_PATH_TEMPLATE])
                templ = template_helper.Template(
                    data[ATTR_PATH_TEMPLATE], hass)
                get_path = template_helper.render_complex(templ, None)
                url = await self.upload_file_from_path(
                    get_path, timeout=timeout)
            elif data.get(ATTR_PATH):
                url = await self.upload_file_from_path(
                    data[ATTR_PATH], timeout=timeout)
            else:
                url = None

            if url is None:
                _LOGGER.error("No path or URL found for file")
                raise FileUploadError("Could not upload file")

            return url

        async def upload_file_from_url(self, url, timeout=None):
            """Upload a file from a URL. Returns a URL.

            uploaded via XEP_0363 and HTTP and returns the resulting URL
            """
            _LOGGER.info("Getting file from %s", url)

            def get_url(url):
                """Return result for GET request to url."""
                return requests.get(
                    url, verify=data.get(ATTR_VERIFY, True), timeout=timeout)
            result = await hass.async_add_executor_job(get_url, url)

            if result.status_code >= 400:
                _LOGGER.error("Could not load file from %s", url)
                return None

            filesize = len(result.content)

            # we need a file extension, the upload server needs a
            # filename, if none is provided, through the path we guess
            # the extension
            # also setting random filename for privacy
            if data.get(ATTR_PATH):
                # using given path as base for new filename. Don't guess type
                filename = self.get_random_filename(data.get(ATTR_PATH))
            else:
                extension = mimetypes.guess_extension(
                    result.headers['Content-Type']) or ".unknown"
                _LOGGER.debug("Got %s extension", extension)
                filename = self.get_random_filename(None, extension=extension)

            _LOGGER.info("Uploading file from URL, %s", filename)

            url = await self['xep_0363'].upload_file(
                filename, size=filesize, input_file=result.content,
                content_type=result.headers['Content-Type'], timeout=timeout)

            return url

        async def upload_file_from_path(self, path, timeout=None):
            """Upload a file from a local file path via XEP_0363."""
            _LOGGER.info('Uploading file from path, %s ...', path)

            if not hass.config.is_allowed_path(path):
                raise PermissionError(
                    "Could not access file. Not in whitelist.")

            with open(path, 'rb') as upfile:
                _LOGGER.debug("Reading file %s", path)
                input_file = upfile.read()
            filesize = len(input_file)
            _LOGGER.debug("Filesize is %s bytes", filesize)

            content_type = mimetypes.guess_type(path)[0]
            if content_type is None:
                content_type = DEFAULT_CONTENT_TYPE
            _LOGGER.debug("Content type is %s", content_type)

            # set random filename for privacy
            filename = self.get_random_filename(data.get(ATTR_PATH))
            _LOGGER.debug("Uploading file with random filename %s", filename)

            url = await self['xep_0363'].upload_file(
                filename, size=filesize, input_file=input_file,
                content_type=content_type, timeout=timeout)

            return url

        def send_text_message(self):
            """Send a text only message to a room or a recipient."""
            try:
                if room:
                    _LOGGER.debug("Joining room %s", room)
                    self.plugin['xep_0045'].join_muc(room, sender, wait=True)
                    self.send_message(
                        mto=room, mbody=message, mtype='groupchat')
                else:
                    _LOGGER.debug("Sending message to %s", recipient)
                    self.send_message(
                        mto=recipient, mbody=message, mtype='chat')
            except (IqError, IqTimeout, XMPPError) as ex:
                _LOGGER.error("Could not send text message %s", ex)
            except NotConnectedError as ex:
                _LOGGER.error("Connection error %s", ex)

        # pylint: disable=no-self-use
        def get_random_filename(self, filename, extension=None):
            """Return a random filename, leaving the extension intact."""
            if extension is None:
                path = pathlib.Path(filename)
                if path.suffix:
                    extension = ''.join(path.suffixes)
                else:
                    extension = ".txt"
            return ''.join(random.choice(string.ascii_letters)
                           for i in range(10)) + extension

        def disconnect_on_login_fail(self, event):
            """Disconnect from the server if credentials are invalid."""
            _LOGGER.warning("Login failed")
            self.disconnect()

        @staticmethod
        def discard_ssl_invalid_cert(event):
            """Do nothing if ssl certificate is invalid."""
            _LOGGER.info("Ignoring invalid SSL certificate as requested")

    SendNotificationBot()
